import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

public class NearestCentroidClassifier {

	private ArrayList<Sample> trainingSet;
	private ArrayList<Sample> testSet;

	private HashMap<String, Centroid> classCentroids;
	private HashMap<String, Double> probabilities;
	private Centroid overallCentroid;

	private ArrayList<Double> standardDeviation;
	private double s0;
	private HashMap<String, Double> m;
	private HashMap<String, ArrayList<Double>> d;
	private HashMap<String, Double> thresholdScale;

	public NearestCentroidClassifier(ArrayList<Sample> trainingSet, ArrayList<Sample> testSet) throws Exception {
		int nAttrs = trainingSet.get(0).getValues().size();

		this.trainingSet = trainingSet;
		this.testSet = testSet;

		this.overallCentroid = new Centroid("overall", nAttrs);
		this.overallCentroid.setOverall(true);

		this.classCentroids = new HashMap<String, Centroid>();

		for (Sample sample : trainingSet) {
			this.addTrainingSample(sample);
		}

		this.calculateCentroids();
		this.calculateProbabilities();
		this.calculateStandardDeviation();
	}

	public double getS(int i) {
		// return this.standardDeviation.get(i);
		return this.standardDeviation.get(i) + this.s0;
	}

	public void initializeThresholdScale() {
		this.thresholdScale = new HashMap<String, Double>();

		Set<String> keys = this.getCentroids().keySet();

		for (String classeK : keys) {
			this.thresholdScale.put(classeK, 1.0);
		}
	}

	public void calculateM() {
		this.m = new HashMap<String, Double>();

		int n = trainingSet.size();

		Set<String> classes = classCentroids.keySet();
		for (String classe : classes) {
			int nk = classCentroids.get(classe).getInstances();

			double mk = Math.sqrt((1.0 / nk) - (1.0 / n));
			m.put(classe, mk);
		}
	}

	public void calculateD() {
		this.d = new HashMap<String, ArrayList<Double>>();

		int nAttrs = trainingSet.get(0).getValues().size();

		Set<String> classes = classCentroids.keySet();
		for (String classeK : classes) {
			Centroid centroidK = classCentroids.get(classeK);
			Double mk = m.get(classeK);
			// Double scaleK = this.thresholdScale.get(classeK);

			ArrayList<Double> dk = new ArrayList<Double>(nAttrs);

			for (int i = 0; i < nAttrs; i++) {
				double xik = centroidK.getValues().get(i);
				double xi = overallCentroid.getValues().get(i);
				double si = this.getS(i);
				double dik = (xik - xi) / (mk * si);
				// double dik = (xik - xi) / (mk * si * scaleK);
				dk.add(dik);
			}

			this.d.put(classeK, dk);
		}
	}

	public void calculateProbabilities() {
		this.probabilities = new HashMap<String, Double>();

		Set<String> keys = classCentroids.keySet();

		for (String classeK : keys) {
			probabilities.put(classeK, classCentroids.get(classeK).getInstances()
					/ (double) this.overallCentroid.getInstances());
		}
	}

	public void addTrainingSample(Sample sample) throws Exception {
		Centroid centroid = this.classCentroids.get(sample.getClasse());
		if (centroid == null) {
			centroid = new Centroid(sample.getClasse(), sample.getValues().size());
			this.addCentroid(centroid);
		}

		centroid.addSample(sample);
		this.overallCentroid.addSample(sample);
	}

	public void calculateCentroids() throws Exception {
		Set<String> keys = classCentroids.keySet();

		for (String key : keys) {
			classCentroids.get(key).calculate();
		}

		overallCentroid.calculate();
	}

	public void calculateStandardDeviation() {
		int size = trainingSet.get(0).getValues().size();

		this.s0 = 0.0;
		this.standardDeviation = new ArrayList<Double>(size);
		for (int i = 0; i < size; i++) {
			this.standardDeviation.add(0.0);
		}

		int aux = this.trainingSet.size() - classCentroids.size(); // n - k

		if (aux != 0) {
			for (Sample sample : this.trainingSet) {
				ArrayList<Double> sampleValues = sample.getValues();

				for (int i = 0; i < size; i++) {
					Centroid centroid = classCentroids.get(sample.getClasse());
					double diff = sampleValues.get(i) - centroid.getValues().get(i);
					double newValue = this.standardDeviation.get(i) + diff * diff;
					this.standardDeviation.set(i, newValue);
				}
			}

			for (int i = 0; i < size; i++) {
				double s = Math.sqrt(this.standardDeviation.get(i) / aux); // (1/(n-k))*s
				this.standardDeviation.set(i, s);
				this.s0 += s;
			}

			this.s0 = this.s0 / this.standardDeviation.size();
		}
	}

	public void addCentroid(Centroid centroid) {
		this.classCentroids.put(centroid.getClasse(), centroid);
	}

	public void printCentroids(DataBase dataBase) {
		System.out.println("Centroids\n");

		System.out.print(dataBase.getClassAttribute());

		ArrayList<String> attributes = dataBase.getAttributes();

		for (int i = 0; i < attributes.size() - 1; i++) {
			System.out.print(String.format("\t%s", attributes.get(i)));
		}

		System.out.println(String.format("\t%s", "instances"));

		Set<String> keys = classCentroids.keySet();
		for (String key : keys) {
			System.out.println(key + "\t" + classCentroids.get(key) + "\t" + classCentroids.get(key).getInstances());
		}

		System.out.println(this.overallCentroid.getClasse() + "\t" + this.overallCentroid + "\t"
				+ this.overallCentroid.getInstances());
	}

	public double euclideanDistance(Sample sample, Centroid centroid) {
		ArrayList<Double> sampleValues = sample.getValues();
		ArrayList<Double> centroidValues = centroid.getValues();

		double euclideanDistance = 0;
		double diff;

		for (int i = 0; i < sampleValues.size(); i++) {
			diff = sampleValues.get(i) - centroidValues.get(i);
			euclideanDistance += diff * diff;
		}

		return Math.sqrt(euclideanDistance);
	}

	public double standardizedSquaredDistance(Sample sample, Centroid centroid) {
		ArrayList<Double> sampleValues = sample.getValues();
		ArrayList<Double> centroidValues = centroid.getValues();

		double standardizedSquaredDistance = 0;
		double diff;

		for (int i = 0; i < sampleValues.size(); i++) {
			double xi = sampleValues.get(i);
			double xik = centroidValues.get(i);
			double si = this.getS(i);
			diff = xi - xik;
			standardizedSquaredDistance += (diff * diff) / (si * si);
		}

		return standardizedSquaredDistance;
	}

	public double minkowskiDistance(Sample sample, Centroid centroid, int n) {
		ArrayList<Double> sampleValues = sample.getValues();
		ArrayList<Double> centroidValues = centroid.getValues();

		double minkowskiDistance = 0;
		double diff;

		for (int i = 0; i < sampleValues.size(); i++) {
			diff = sampleValues.get(i) - centroidValues.get(i);
			minkowskiDistance += Math.pow(diff, n);
		}

		return Math.pow(minkowskiDistance, 1 / (double) n);
	}

	public String classify(Sample sample) {
		String estimatedClass = null;
		double smallestDistance = 1 / 0.0;

		Set<String> classes = this.classCentroids.keySet();
		for (String classe : classes) {
			Centroid centroid = this.classCentroids.get(classe);

			double distance = standardizedSquaredDistance(sample, centroid);
			// double distance = euclideanDistance(sample, centroid);
			// double distance = minkowskiDistance(sample, centroid, 2);
			distance -= 2 * Math.log10(this.probabilities.get(classe));
			if (distance < smallestDistance) {
				smallestDistance = distance;
				estimatedClass = classe;
			}
		}

		return estimatedClass;
	}

	public double classifyTestSet() {
		double errors = 0;

		for (Sample testSample : this.testSet) {
			String estimatedClass = this.classify(testSample);

			if (!estimatedClass.equals(testSample.getClasse())) {
				errors++;
			}
		}

		errors = errors / testSet.size();

		return errors;
	}

	public ArrayList<Sample> getTrainingSet() {
		return trainingSet;
	}

	public void setTrainingSet(ArrayList<Sample> trainingSet) {
		this.trainingSet = trainingSet;
	}

	public ArrayList<Sample> getTestSet() {
		return testSet;
	}

	public void setTestSet(ArrayList<Sample> testSet) {
		this.testSet = testSet;
	}

	public HashMap<String, Centroid> getCentroids() {
		return classCentroids;
	}

	public void setCentroids(HashMap<String, Centroid> centroids) {
		this.classCentroids = centroids;
	}

	public Centroid getOverallCentroid() {
		return overallCentroid;
	}

	public void setOverallCentroid(Centroid overallCentroid) {
		this.overallCentroid = overallCentroid;
	}

	public ArrayList<Double> getStandardDeviation() {
		return standardDeviation;
	}

	public void setStandardDeviation(ArrayList<Double> standardDeviation) {
		this.standardDeviation = standardDeviation;
	}

	public HashMap<String, Double> getProbabilities() {
		return probabilities;
	}

	public void setProbabilities(HashMap<String, Double> probabilities) {
		this.probabilities = probabilities;
	}

	public HashMap<String, Double> getM() {
		return m;
	}

	public void setM(HashMap<String, Double> m) {
		this.m = m;
	}

	public HashMap<String, ArrayList<Double>> getD() {
		return d;
	}

	public void setD(HashMap<String, ArrayList<Double>> d) {
		this.d = d;
	}

	public HashMap<String, Double> getThresholdScale() {
		return thresholdScale;
	}

	public void setThresholdScale(HashMap<String, Double> thresholdScale) {
		this.thresholdScale = thresholdScale;
	}
}
